package javax.per.toolkit.xml;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.per.toolkit.tools.Log;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Test for reading and writing xml file with w3c DOM
 *<br/><br/>
 *<strong>Test code</strong><br/>
 * 	public static void main(String[] args) throws Exception {<br/>
 *		DOMReadAndWrite dom=new DOMReadAndWrite();<br/>
 *		dom.setXmlPath("src/aaa.xml");<br/>
 *		dom.init();<br/>
 *		dom.display();<br/>
 *		dom.removeAll();<br/>
 *		dom.display();<br/>
 *		dom.store();<br/>
 *		HashMap<String,String> map=new HashMap<String,String>();<br/>
 *		map.put("author", "aaa");<br/>
 *		map.put("price", "10");<br/>
 *		map.put("date", "2013");<br/>
 *		dom.add("book", map);<br/>
 *		dom.store();<br/>
 *	}<br/>
 *@version 1.0
 *@author yl34569
 */

public class DOMOperator {

	/**
	 * @param filePath path of a file.
	 * @throws ParserConfigurationException 
	 * @throws IOException 
	 * @throws SAXException 
	 * @throws TransformerException 
	 */
	public DOMOperator(String filePath){
		this.xmlPath=filePath;
		try {
			this.init();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * @param file a file.
	 */
	public DOMOperator(File file){
		this.xmlPath=file.getAbsolutePath();
		try {
			this.init();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * Type of traversal.
	 * @author yl34569
	 */
	public enum tMode{DEPTH_FIRST_TRAVERSAL,BREADTH_FIRST_TRAVERSAL};
	private String xmlPath=null;
	private Document doc=null;
	
	/**
	 * The mode of traversal 
	 */
	private tMode traversalMode=tMode.DEPTH_FIRST_TRAVERSAL;
	
	/**
	 * The encoding used when stored.
	 */
	private String outputEncoding="UTF-8";
	
	/**
	 * Whether auto-add indent when stored.
	 */
	private boolean outputIndent=true;
	
	/**
	 * Create XML file if it's not exist,throw Exception if it's already exist
	 * @param rootElement root element of the XML file
	 * @throws Exception
	 */
	public void createXmlFile(String rootElement) throws Exception{
		if(xmlPath==null)
			throw new Exception("XML file path not set");
		File f=new File(xmlPath);
		if(f.exists())
			throw new Exception("XML file already exist");
		f.getParentFile().mkdirs();
		f.createNewFile();
		OutputStream os=new FileOutputStream(f);
		String str="<?xml version=\"1.0\" encoding=\"UTF-8\"?> <"+rootElement+"/>";
		os.write(str.getBytes());
		os.close();
	}
	
	/**
	 * Traversal XML file from root node.
	 * @param ope the operation used in node.
	 */
	protected void traversalRoot(INodeOperator ope){
		Node root=doc.getDocumentElement();
		if(root.hasChildNodes()){
			if(traversalMode==tMode.DEPTH_FIRST_TRAVERSAL){
				ope.operator(root);
				traversalNodeDepth(root,ope);
			}else if(traversalMode==tMode.BREADTH_FIRST_TRAVERSAL){
				traversalNodeBreadth(root,ope);
			}
		}
	}
	
	/**
	 * Depth first traversal.
	 * @param node a node.
	 * @param ope the operation used in node.
	 */
	protected void traversalNodeDepth(Node node,INodeOperator ope){
		NodeList list=node.getChildNodes();
		for(int i=0;i<list.getLength();i++){
			Node tmp=list.item(i);
			ope.operator(tmp);
			if(tmp.hasChildNodes())
				traversalNodeDepth(tmp,ope);
		}
	}
	
	/**
	 * Breadth first traversal.
	 * @param node a node.
	 * @param ope the operation used in node.
	 */
	protected void traversalNodeBreadth(Node node,INodeOperator ope){
		ArrayDeque<Node> que=new ArrayDeque<Node>();
		que.addLast(node);
		while(!que.isEmpty()){
			Node n=que.poll();
			ope.operator(n);
			if(n.hasChildNodes()){
				NodeList li=n.getChildNodes();
				for(int i=0;i<li.getLength();i++){
					que.addLast(li.item(i));
				}
			}
		}
	}
	
	/**
	 * Finds a specialized node in a XML file.
	 * @param nodeName node name.
	 * @return list of found.
	 */
	public List<Node> findNodes(String[] nodeName){
		INodeOperator ope=new NodeFindOperator(nodeName);
		this.traversalRoot(ope);
		return ((NodeFindOperator)ope).getResult();
	}
	
	/**
	 * Print some nodes.
	 * @param li nodes list.
	 */
	public void print(List<Node> li){
		pl("---------------------------------------------------");
		for(Node node:li){
			pl("Node name\t"+node.getNodeName());
			pl("Node type\t"+node.getNodeType());
			pl("Node value\t"+node.getNodeValue());
			
//			pl("Node text\t"+node.getTextContent());
			
			NamedNodeMap attr=node.getAttributes();
			if(attr!=null){
				System.out.print("Node attrs\t");
				for(int i=0;i<attr.getLength();i++){
					Node n=attr.item(i);
					System.out.print(n.getNodeName()+"="+n.getNodeValue()+"\t");
				}
				pl("");
			}
			pl("---------------------------------------------------");
		}
	}
	
	/**
	 * Print a XML file.
	 */
	public void print(){
		INodeOperator ope=new INodeOperator(){
			@Override
			public void operator(Node node) {
				pl("Node name\t"+node.getNodeName());
				pl("Node type\t"+node.getNodeType());
				pl("Node value\t"+node.getNodeValue());
//				pl("Node text\t"+node.getTextContent());
				NamedNodeMap attr=node.getAttributes();
				if(attr!=null){
					System.out.print("Node attrs\t");
					for(int i=0;i<attr.getLength();i++){
						Node n=attr.item(i);
						System.out.print(n.getNodeName()+"="+n.getNodeValue()+"\t");
					}
					pl("");
				}
				pl("---------------------------------------------------");
			}
			
		};
		pl("---------------------------------------------------");
		this.traversalRoot(ope);
	}
	
	/**
	 * Connect two XML files.
	 * @param xmlFile the second file.
	 * @throws SAXException
	 * @throws IOException
	 * @throws ParserConfigurationException
	 */
	public void connect(File xmlFile) throws SAXException, IOException, ParserConfigurationException{
		DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance();
		DocumentBuilder builder=factory.newDocumentBuilder();
		Document doc1=builder.parse(xmlFile);
		Element r=doc1.getDocumentElement();
		NodeList nl=r.getChildNodes();
		Element root=doc.getDocumentElement();
		for(int i=0;i<nl.getLength();i++){
			root.appendChild(nl.item(i));
		}
		store();
	}
	
	/**
	 * Store DOMs in a XML file
	 */
	public  void store(){
		if(!Charset.isSupported(this.outputEncoding))
			throw new Error("Encoding not supported.");
		try{
		TransformerFactory factory=TransformerFactory.newInstance();
		Transformer trans=factory.newTransformer();
		trans.setOutputProperty(OutputKeys.ENCODING, outputEncoding);
		trans.setOutputProperty(OutputKeys.INDENT, outputIndent?"yes":"no");
		OutputStream out=new FileOutputStream(new File(xmlPath));
		DOMSource source=new DOMSource(doc);
		StreamResult result=new StreamResult(out);
		trans.transform(source, result);
		}catch(Exception e){
			e.printStackTrace();
		}
	}
	
	/**
	 * Add an element in DOMS
	 * @param elementOuter outer element
	 * @param elementInner inner element set
	 */
	public  void add(String elementOuter,HashMap<String,String> elementInner){
		Element outer=doc.createElement(elementOuter);
		Iterator<String> it=elementInner.keySet().iterator();
		while(it.hasNext()){
			String key=it.next();
			Element ele=doc.createElement(key);
//			ele.setTextContent(elementInner.get(key));
			outer.appendChild(ele);
		}
		doc.getDocumentElement().appendChild(outer);
	}
	
	public void add(String element,Map<String,String> attrib){
		Element ele=doc.createElement(element);
		Iterator<String> it=attrib.keySet().iterator();
		while(it.hasNext()){
			String key=it.next();
			ele.setAttribute(key, attrib.get(key));
		}
		doc.getDocumentElement().appendChild(ele);
	}
	
	/**
	 * Remove all elements
	 */
	public  void removeAll(){
		NodeList list=doc.getDocumentElement().getChildNodes();
		for(int i=list.getLength()-1;i>=0;i--){
			doc.getDocumentElement().removeChild(list.item(i));
		}
	}
	
	/**
	 * Remove document element
	 * @deprecated can't recovery.
	 */
	public  void removeDocumentElement(){
		doc.removeChild(doc.getDocumentElement());
	}
	
	/**
	 * Setting of XML path
	 * @param xmlPath path of the XML file
	 * @throws Exception 
	 */
	public void changeXmlPath(String xmlPath) throws Exception {
		this.xmlPath = xmlPath;
		this.init();
	}

	/**
	 * Initialization
	 * @throws Exception
	 */
	public void init() throws Exception{
		if(xmlPath==null)
			throw new Exception("XML file path not set");
		File f=new File(xmlPath);
		if(!f.exists())
			throw new Exception("XML file not exist");
		DocumentBuilderFactory factory=DocumentBuilderFactory.newInstance();
		DocumentBuilder builder=factory.newDocumentBuilder();
		doc=builder.parse(f);
	}

	public tMode getTraversalMode() {
		return traversalMode;
	}

	public void setTraversalMode(tMode traversalMode) {
		this.traversalMode = traversalMode;
	}

	public String getOutputEncoding() {
		return outputEncoding;
	}

	public void setOutputEncoding(String outputEncoding) {
		this.outputEncoding = outputEncoding;
	}

	public boolean isOutputIndent() {
		return outputIndent;
	}

	public void setOutputIndent(boolean outputIndent) {
		this.outputIndent = outputIndent;
	}

	public String getXmlPath() {
		return xmlPath;
	}

	/**
	 * Example for using this class.
	 * @param args command line.
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {
 		DOMOperator dom=new DOMOperator("src/aaa.xml");
 		dom.print();
 		dom.removeAll();
 		dom.print();
 		dom.store();
 		HashMap<String,String> map=new HashMap<String,String>();
 		map.put("author", "aaa");
 		map.put("price", "10");
 		map.put("date", "2013");
 		dom.add("book", map);
 		dom.store();
 	}
	
	private void pl(Object obj){
		Log.getLogger().debug(obj);
	}
	
}

